import { ObjectTypes } from './constant'
import {ArrayType, BooleanType, ErrorType, FunctionCallType, FunctionLiteralType, HashType, IntegerType, NullType, ReturnType, StringType} from './model/types'
import Enviroment from './model/enviroment'
import { isNotNull, isNull } from '../utils/'

export default class MonkeyEvaluator {

    constructor() {
        this.enviroment = new Enviroment()
    }

    newEnclosedEnvironment(outerEnv) {
        const env = new Enviroment()
		env.outer = outerEnv
		return env
    }

    eval(node) {
        switch(node.type) {
            case "program":
                return this.evalProgram(node)
            case "ArrayLiteral":
                const elements = this.evalExpressions(node.elements)
                if (elements.length === 1 && this.isError(elements[0])) {
                    return elements[0]
                }
                return new ArrayType(elements)
            case "LetStatement":
                let val = this.eval(node.value)
                if (this.isError(val)) {
                    return val
                }
                this.enviroment.set(node.name.literal, val)
                return val
            case "Identifier":
                console.log("variable name is:" + node.literal)
                const value = this.evalIdentifier(node, this.enviroment)
                console.log("it is binding value is " + value.inspect())
                return value
            case "FunctionLiteral":
                const env = this.newEnclosedEnvironment(this.enviroment)
                return new FunctionLiteralType(node.token, node.parameters, node.body, env)
            case "CallExpression":
                console.log("evalute function call params:")
                const args = this.evalExpressions(node.args)
                if (args.length === 1 && this.isError(args[0])) {
                    return args[0]
                }
                args.forEach(it => console.log(it.inspect()))
                console.log("execute a function with content:", node.fun.literal)
                const funLiteralType = this.eval(node.fun)
                if (this.isError(funLiteralType)) {
                    return this.buildins(node.fun.literal, args)
                }
                const funcCallType = new FunctionCallType(args, funLiteralType)
                const result = this.evalFuncCall(funcCallType, this.enviroment)
                if (result.type() === ObjectTypes.RETURN_VALUE_OBJECT) {
                    console.log("function call return with :", result.value.inspect())
                    return result.value
                }
                return result
            case "Integer":
                console.log("Integer with value:", node.value)
                return new IntegerType(node.value)
            case "Boolean":
                console.log("Boolean with value:", node.value)
                return new BooleanType(node.value)
            case "String":
                console.log("String with value:", node.value)
                return new StringType(node.value)
            case "ExpressionStatement":
                return this.eval(node.expression)
            case "PrefixExpression":
                const right = this.eval(node.right)
                if (this.isError(right)) {
                    return right
                }
                const obj = this.evalPrefixExpression(node.operator, right)
                console.log("eval prefix expression: ", obj.inspect())
                return obj
            case "InfixExpression":
                const leftExpr = this.eval(node.left)
                const rightExpr = this.eval(node.right)
                return this.evalInfixExpression(node.operator, leftExpr, rightExpr)
            case "IfExpression":
                return this.evalIfExpression(node)
            case "BlockStatement":
                return this.evalStatements(node)
            case "IndexExpression":
                const left = this.eval(node.left)
                if (this.isError(left)) {
                    return left
                }
                const index = this.eval(node.index)
                if (this.isError(index)) {
                    return index
                }
                const indexObj = this.evalIndexExpression(left, index)
                if (indexObj !== null) {
                    console.log(`the ${index.value}th element of array is ${indexObj.inspect()}`)
                }
                return indexObj
            case "ReturnStatement":
                const valueObj = this.eval(node.expression)
                if (this.isError(valueObj)) {
                    return valueObj
                }
                const returnObj = new ReturnType(valueObj)
                console.log(returnObj.inspect())
                return returnObj
            case "HashLiteral":
                return this.evalHashLiteral(node)
            default:
                return new NullType({})
        }
    }

    evalHashLiteral(node) {
        /**
            先递归的解析哈希表的key，然后解析它的value,对于如下类型的哈希表代码
            let add = fn (x, y) { return x+y};
            let byOne = fn (z) { return z+1;}
            {add(1,2) : byOne(3)}
            编译器先执行add(1,2)得到3，然后执行byOne(3)得到4
        */
        const keys = []
        const values = []
        for (let i = 0; i < node.keys.length; i++) {
            const key = this.eval(node.keys[i])
            if (this.isError(key)) {
                return key
            }

            if (this.hashable(key) !== true) {
                return this.newError("unhashable type:", key.type())
            }

            const value = this.eval(node.values[i])
            if (this.isError(value)) {
                return value
            }
            keys.push(key)
            values.push(value)
        }
        const hashObj = new HashType(keys, values)
        console.log(`eval hash object: ${hashObj.inspect()}`)
        return hashObj
    }

    hashable(node) {
        if (node.type() === ObjectTypes.INTEGER_OBJ || node.type() === ObjectTypes.STRING_OBJ || node.type() === ObjectTypes.BOOLEAN_OBJ) {
            return true
        }
        return false
    }


    evalIndexExpression(left, index) {
        if (left.type() === ObjectTypes.ARRAY_OBJ && index.type() === ObjectTypes.INTEGER_OBJ) {
            return this.evalArrayIndexExpression(left, index)
        }
        if (left.type() === ObjectTypes.HASH_OBJ && this.hashable(index)) {
            return this.evalHashIndexExpression(left, index)
        }
    }

    evalArrayIndexExpression(array, index) {
        const idx = index.value
        const max = array.elements.length - 1
        if (idx < 0 || idx > max) {
            return null
        }
        return array.elements[idx]
    }

    evalHashIndexExpression(hash, key) {
        if (!this.hashable(key)) {
            return this.newError("unhashable type:", key.type())
        }
        for(let i = 0; i < hash.keys.length; i++) {
            if (hash.keys[i].value === key.value) {
                console.log(`return hash value: ${hash.values[i]}`)
                return hash.values[i]
            }
        }
        return null
    }



    evalFuncCall(funcCallType, oldEnviroment) {
        // 函数定义
        const funLiteralType = funcCallType.functionLiteral
        // 设置新的变量绑定环境
        this.enviroment = funLiteralType.enviroment
        //将输入参数名称与传入值在新环境中绑定
        for (let i = 0; i < funLiteralType.identifiers.length; i++) {
            // 形参
            const name = funLiteralType.identifiers[i].literal
            // 实参
            const val = funcCallType.args[i]
            this.enviroment.set(name, val)
        }
        //执行函数体内代码
        const result =  this.eval(funLiteralType.blockStatement)
        //执行完函数后，里面恢复原有绑定环境
        this.enviroment = oldEnviroment
        return result
    }
    evalExpressions(exps) {
        const result = []
		for(let i = 0; i < exps.length; i++) {
			var evaluated = this.eval(exps[i])
			if (this.isError(evaluated)) {
				return evaluated
			}
			result[i] = evaluated
		}
		return result
    }

    evalIdentifier(node, env) {
        const val = env.get(node.literal)
        if (isNull(val)) {
            return this.newError("identifier no found:"+node.name)
        }
        return val
    }

    evalProgram(program) {
        let result = null
        for(let stms of program.statements) {
            result = this.eval(stms)
            if (result.type() === ObjectTypes.RETURN_VALUE_OBJECT) {
                return result.value
            }
            if (result.type() === ObjectTypes.NULL_OBJ) {
                return result
            }
            if (result.type() === ObjectTypes.ERROR_OBJ) {
                console.log(result.msg)
                return result
            }
        }
        return result
    }

    evalIfExpression(ifNode) {
        console.log('begin to eval if statement')
        const condition = this.eval(ifNode.condition)
        if (this.isError(condition)) {
            return condition
        }
        if (this.isTruthy(condition)) {
            console.log('condition in if holds, exec statements in if block')
            return this.eval(ifNode.consequence)
        } else if(isNotNull(ifNode.alternative)) {
            console.log('condition in if no holds, exec statements in else block')
            return this.eval(ifNode.alternative)
        } else {
            console.log('condition in if no holds, exec nothing!')
            return null
        }
    }

    isTruthy(condition) {
        if (condition.type() === ObjectTypes.INTEGER_OBJ) {
            if (condition.value !== 0) {
                return true
            }
            return false
        }
        if (condition.type() === ObjectTypes.BOOLEAN_OBJ) {
            return condition.value
        }
        if (condition.type() === ObjectTypes.NULL_OBJ) {
            return false
        }
        return true
    }

    evalStatements(node) {
        let result = null
        for (let i = 0; i < node.statements.length; i++) {
            const stmt = node.statements[i]
            result = this.eval(stmt)
            if (result.type() === ObjectTypes.RETURN_VALUE_OBJECT || result.type() === ObjectTypes.ERROR_OBJ) {
                return result
            }
        }
        return result
    }


    evalInfixExpression(operator, left, right) {
        if (left.type() !== right.type()) {
            return this.newError(`type mismatch: ${left.type()} and ${right.type()}`)
        }
        if(left.type() === ObjectTypes.INTEGER_OBJ && right.type() === ObjectTypes.INTEGER_OBJ) {
            return this.evalIntegerInfixExpression(operator, left, right)
        }
        if (left.type() === ObjectTypes.STRING_OBJ && right.type() === ObjectTypes.STRING_OBJ) {
            return this.evalStringInfixExpression(operator, left, right)
        }
        return null
    }

    evalIntegerInfixExpression(operator, left, right) {
        const leftVal = left.value
        const rightVal = right.value
        let val = null
        let resultType = "integer"
        switch(operator) {
            case "+":
                val = leftVal + rightVal
                break
            case "-":
                val = leftVal - rightVal
                break
            case "*":
                val = leftVal * rightVal
                break
            case "/":
                val = leftVal / rightVal
                break
            case "==":
                resultType = "boolean"
                val = (leftVal === rightVal)
                break
            case "!=":
                resultType = "boolean"
                val = (leftVal !== rightVal)
                break
            case ">":
                resultType = "boolean"
                val = (leftVal > rightVal)
                break
            case "<":
                resultType = "boolean"
                val = (leftVal < rightVal)
                break
            default:
                return null
        }
        console.log(`eval infix expression result is: ${val}`)
        let result = null
        if (resultType === "integer") {
            result = new IntegerType(val)
        } else if (resultType === "boolean") {
            result = new BooleanType(val)
        }
        return result
    }

    evalStringInfixExpression(operator, left, right) {
        if (operator !== "+") {
            return this.newError("unknown operator for string type")
        }
        let leftVal = left.value
        let rightVal = right.value
        const val = leftVal + rightVal
        console.log("result of string add is:", val)
        return new StringType(val)
    }

    evalPrefixExpression(operator, right) {
        switch (operator) {
            case "!":
                return this.evalBangOperatorExpression(right)
            case "-":
                return this.evalMinusPrefixOperatorExpression(right)
            default:
                return this.newError("unknown operator:", operator, right.type())
        }
    }

    buildins(name, args) {
        switch(name) {
            case "first":
                if (args.length !== 1) {
                    return this.newError("Wrong number of arguments when calling len")
                }
                if (args[0].type() !== ObjectTypes.ARRAY_OBJ) {
                    return this.newError("arguments of first must be ARRAY")
                }
                if (args[0].elements.length > 0) {
                    console.log("the first element of array is :", args[0].elements[0].inspect())
                    return args[0].elements[0]
                }
                return null
            case "rest":
                if (args.length !== 1) {
                    return this.newError("Wrong number of arguments when calling len")
                }
                if (args[0].type() !== ObjectTypes.ARRAY_OBJ) {
                    return this.newError("arguments of first must be ARRAY")
                }
                if (args[0].elements.length > 1) {
                    const elements = args[0].elements.slice(1)
                    const obj = new ArrayType(elements)
                    console.log("rest return: ", obj.inspect())
                    return obj
                }
                return null
            case "append":
                if (args.length !== 2) {
                    return this.newError("Wrong number of arguments when calling len")
                }
                if (args[0].type() !== ObjectTypes.ARRAY_OBJ) {
                    return this.newError("arguments of first must be ARRAY")
                }
                const newArr = new ArrayType([...args[0].elements, args[1]])
                console.log("new array after calling append is: ", newArr.inspect())
			    return newArr
            case "len":
                if (args.length !== 1) {
                    return this.newError("Wrong number of arguments")
                }
                switch(args[0].type()) {
                    case ObjectTypes.STRING_OBJ:
                        const obj_int = new IntegerType(args[0].value.length)
                        console.log("API len return: ", obj_int.inspect())
                        return obj_int
                    case ObjectTypes.ARRAY_OBJ:
                        const obj_arr = new IntegerType(args[0].elements.length)
                        console.log("API len return: ", obj_arr.inspect())
                        return obj_arr
                    default:
                        return null
                }
            default:
                return this.newError("unknown function call")
        }
    }

    isError(obj) {
        if (isNotNull(obj)) {
			return obj.type() === ObjectTypes.ERROR_OBJ
		}
		return false
    }

    evalBangOperatorExpression(right) {
        if (right.type() === ObjectTypes.BOOLEAN_OBJ) {
            if (right.value === true) {
                return new BooleanType(false)
            }
            if (right.value === false) {
                return new BooleanType(true)
            }
        }
        if (right.type() === ObjectTypes.INTEGER_OBJ) {
            if (right.value === 0) {
                return new BooleanType(true)
            } else {
                return new BooleanType(false)
            }
        }
        if (right.type() === ObjectTypes.NULL_OBJ) {
			return new BooleanType(true)
		}
        return this.newError("unknown type:", right.type())
    }

    evalMinusPrefixOperatorExpression(right) {
        if (right.type() !== ObjectTypes.INTEGER_OBJ) {
			return this.newError("unknown operaotr:- ", right.type())
		}
        return new IntegerType(-right.value)
    }

    newError(msg, type) {
        return new ErrorType(msg + type)
    }
}